﻿//-------------------------------------
// WBFSSync - WBFSSync.exe
//
// Copyright 2009 Caian (ÔmΣga Frøst) <frost.omega@hotmail.com> :
//
// WBFSSync is Licensed under the terms of the Microsoft Reciprocal License (Ms-RL)
//
// Covers.cs:
//
// Gerenciador de capas de jogos, download da internet e sincronizador de capas com SD's
//
// Nota: Tanto a exportação de capas quanto o download são feitos pelo OperationManager,
// para o caso do download a rotina de download individual fica aqui
//
//-------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.IO;
using System.Net;
using System.Globalization;

namespace WBFSSync
{
    //--------------------- Estrutura para as capas carregadas
    public struct LoadedCover
    {
        public string Code;
        public Image Image;

        public LoadedCover(String c, Image i)
        {
            Code = c;
            Image = i;
        }
    }

    //--------------------- Classe para os Loaders do Wii
    public class LoaderHomebrew
    {
        public String Name;
        public String Path; //Se estiver vazio as capas são jogadas na raiz

        public Int32 CoverWidth;
        public Int32 CoverHeight;

        public static Boolean TryParse(String s, out LoaderHomebrew result)
        {
            //Formato: Nome|diretório|largura da capa|altura da capa

            result = null; //Só por garantia...

            String[] args = s.Split(new char[]{'|'}, StringSplitOptions.RemoveEmptyEntries);
            if (args.Length != 4)
            {
                //Loga erro
                Log.LogLine(Language.log_parsecovererror_name, LogMessageType.Error);

                args = null;
                return false;
            }

            if (args[1].Length > 0)
            {
                args[1] = args[1].Replace('/', '\\');

                if(args[1][args[1].Length - 1] != '\\') args[1] += '\\';
                if (args[1][0] == '\\') args[1] = args[1].Remove(0, 1);
            }

            int width, height;

            if (!Int32.TryParse(args[2], out width))
            {
                //Loga erro
                Log.LogLine(Language.log_parsecovererror_dimension, LogMessageType.Error, args[0]);

                args = null;
                return false;
            }

            if (!Int32.TryParse(args[3], out height))
            {
                //Loga erro
                Log.LogLine(Language.log_parsecovererror_dimension, LogMessageType.Error, args[0]);

                args = null;
                return false;
            }

            if (width == 0) width = Covers.DefaultWidth;
            if (height == 0) height = Covers.DefaultHeight;

            result = new LoaderHomebrew();
            result.Name = args[0];
            result.Path = args[1];
            result.CoverWidth = width;
            result.CoverHeight = height;

            return true;
        }

        public LoaderHomebrew()
        {
            Name = "";
            Path = "";
            CoverHeight = 0;
            CoverWidth = 0;
        }

        public LoaderHomebrew(String n, String p, int w, int h)
        {
            Name = n;
            Path = p;
            CoverHeight = h;
            CoverWidth = w;
        }

        public override string ToString()
        {
            //Formato: Nome|diretório|largura da capa|altura da capa
            return String.Format("{0}|{1}|{2}|{3}", Name, Path, CoverWidth, CoverHeight);
        }
    }

    static class Covers
    {
        //--------------------- Constantes

        //Diretórios
        public const string CoversDir2D = @"Covers\2D\";
        public const string ThumbsDir2D = @"Covers\Thumbs\2D\";

        public const string CoversDir3D = @"Covers\3D\";
        public const string ThumbsDir3D = @"Covers\Thumbs\3D\";

        //Diretórios
        public static string CoversDir = @"Covers\";
        public static string ThumbsDir = @"Covers\Thumbs\";

        //Download
        public const string DownloadHost = "http://wiitdb.com/";

        //Tamanho usado pelo programa
        public const int CoverWidth = 51;
        public const int CoverHeight = 72;

        //Tamanho padrão
        public const int DefaultWidth = 160;
        public const int DefaultHeight = 225;

        //--------------------- Variáveis

        //Monitor
        public static FileSystemWatcher Watcher = null;

        //Capas
        public static List<String> AvaliableCovers = new List<string>(); //TODAS as capas encontradas, carregadas ou não
        public static List<LoadedCover> LoadedCovers = new List<LoadedCover>(); //Capas carregadas (automaticamente por uso)
        public static List<LoadedCover> FullSizeImages = new List<LoadedCover>(); //Capas tamanho grande, usadas somente
                                                                                  //para a pasta 'Capas' no explorer

        public static event EventHandler CoverListChanged;

        //Download
        public static List<String> CoversToDownload = new List<string>(); //Códigos das capas a serem baixadas
        public static List<String> FailedDownloads = new List<string>(); //Impede que capas que nao existem ou uma falha
                                                                         //faça o programa tentar baixar as capas a cada
                                                                         //sincronização de capas, essa lista não é salva

        public static Boolean Download3DCovers = false;
        public static Boolean Download3DCoversCfg = false;

        public static Boolean CoverDownloadActive = false;
        public static Boolean AutoDownloadCovers = true;

        //Loaders
        public static List<LoaderHomebrew> Loaders = new List<LoaderHomebrew>(); //Homebrews para sincronizar as capas
        public static Boolean LookForCompatiblePath = true; //Na hora de exportar as capas o programa procura pelo
                                                            //primeiro dispositivo que possua o diretório especificado
                                                            //pelo LoaderHomebrew.Path antes de exibir o diálogo de busca

        public static String UserRegion { get { return CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; } }
        
        //----------------------- Inicializa a classe, listando todas as capas disponíveis
        public static void Initialize()
        {
            if (!Directory.Exists(CoversDir)) Directory.CreateDirectory(CoversDir);
            if (!Directory.Exists(ThumbsDir)) Directory.CreateDirectory(ThumbsDir);
            String coverfull = Path.GetFullPath(CoversDir);

            Watcher = new FileSystemWatcher(coverfull, "*.png");

            Watcher.Changed += new FileSystemEventHandler(Watcher_Changed);
            Watcher.Created += new FileSystemEventHandler(Watcher_Changed);
            Watcher.Deleted += new FileSystemEventHandler(Watcher_Changed);
            Watcher.Renamed +=new RenamedEventHandler(Watcher_Renamed);

            Watcher.NotifyFilter = NotifyFilters.FileName;
            Watcher.IncludeSubdirectories = false;
            Watcher.EnableRaisingEvents = true;

            String[] fCovers = Directory.GetFiles(CoversDir, "*.png");
            String[] tCovers = Directory.GetFiles(ThumbsDir, "*.png");

            for (int i = 0; i < fCovers.Length; i++)
            {
                String thumb = ThumbsDir + Path.GetFileName(fCovers[i]);
                if (!tCovers.Contains(thumb))
                {
                    ResizeImage.ResizeFile(fCovers[i], thumb, CoverWidth, CoverHeight);
                }

                AvaliableCovers.Add(thumb);
            }
        }

        //----------------------- Evento ativado quando uma capa é renomeada
        static void Watcher_Renamed(object sender, RenamedEventArgs e)
        {
            if (IOManager.ContainsHandle(e.FullPath)) return; //Arquivo em uso pelo sistema

            if (File.Exists(ThumbsDir + e.OldName))
            {
                File.Move(ThumbsDir + e.OldName, ThumbsDir + e.Name);
            }
            else
            {
                ResizeImage.ResizeFile(CoversDir + e.Name, ThumbsDir + e.Name, CoverWidth, CoverHeight);
            }

            int c = -1;
            if ((c = CoverLoaded(Path.GetFileNameWithoutExtension(e.OldName))) >= 0)
            {
                LoadedCover newcover = LoadedCovers[c];
                newcover.Image.Dispose();

                newcover.Image = Image.FromFile(ThumbsDir + e.Name);
                newcover.Code = Path.GetFileNameWithoutExtension(e.Name);

                LoadedCovers[c] = newcover;
            }

            int index = AvaliableCovers.IndexOf(ThumbsDir + e.OldName);
            if (index >= 0)
            {
                AvaliableCovers[index] = ThumbsDir + e.Name;
            }

            OnCoverListChanged();
        }

        //----------------------- Evento ativado quando uma capa é alterada
        static void Watcher_Changed(object sender, FileSystemEventArgs e)
        {
            if (IOManager.ContainsHandle(e.FullPath)) return; //Arquivo em uso pelo sistema

            String thumb = ThumbsDir + e.Name;
            String cover = CoversDir + e.Name;
            int c = 0;
            switch (e.ChangeType)
            {
                case WatcherChangeTypes.Changed:

                    if(File.Exists(thumb)) IOManager.Delete(thumb);
                    ResizeImage.ResizeFile(cover, thumb, CoverWidth, CoverHeight);

                    c = CoverLoaded(Path.GetFileNameWithoutExtension(e.Name));
                    if (c >= 0)
                    {
                        LoadedCover newcover = LoadedCovers[c];
                        newcover.Image.Dispose();
                        newcover.Image = Image.FromFile(thumb);
                        LoadedCovers[c] = newcover;
                    }
                    break;
                case WatcherChangeTypes.Created:

                    /*ResizeImage.ResizeFile(cover, thumb, CoverWidth, CoverHeight);
                    AvaliableCovers.Add(thumb);
                    */
                    break;
                case WatcherChangeTypes.Deleted:
                    c = CoverLoaded(Path.GetFileNameWithoutExtension(e.Name));
                    if (c >= 0)
                    {
                        LoadedCovers[c].Image.Dispose();
                        LoadedCovers.RemoveAt(c);
                    }

                    c = AvaliableCovers.IndexOf(thumb);
                    if (c >= 0)
                    {
                        AvaliableCovers.RemoveAt(c);
                    }

                    if (File.Exists(thumb))
                    {
                        IOManager.Delete(thumb);
                    }
                    break;
            }

            OnCoverListChanged();
        }

        //----------------------- Verifica se a capa já foi carregada
        public static int CoverLoaded(String code)
        {
            for (int i = 0; i < LoadedCovers.Count; i++) 
                if (String.Compare(LoadedCovers[i].Code, code, true) == 0) 
                    return i;
            
            return -1;
        }

        //----------------------- Verifica se a capa em tamanho grande está carregada
        public static int FullSizeCoverLoaded(String code)
        {
            for (int i = 0; i < FullSizeImages.Count; i++)
                if (String.Compare(FullSizeImages[i].Code, code, true) == 0)
                    return i;

            return -1;
        }

        //----------------------- Retona uma capa já carregada ou tenta carregá-la
        public static Image GetCover(String code, bool tryload)
        {
            foreach (LoadedCover cover in LoadedCovers) if (String.Compare(cover.Code, code, true) == 0) return cover.Image;
            
            if (tryload)
            {
                int load = LoadCover(code);
                if (load >= 0) return LoadedCovers[load].Image;
            }

            return Skin.cover_nocover;
        }

        //----------------------- Carrega uma capa
        static int LoadCover(String code)
        {
            foreach (String cover in AvaliableCovers)
            {
                if (String.Compare(Path.GetFileNameWithoutExtension(cover), code, true) == 0)
                {
                    try
                    {
                        LoadedCover lc = new LoadedCover(code, Image.FromFile(cover));
                        LoadedCovers.Add(lc);
                        return LoadedCovers.Count - 1;
                    }
                    catch (Exception e)
                    {
                        Log.LogException(e);
                    }
                }
            }

            return -1;
        }

        //----------------------- Apaga permanentemente uma capa
        public static void DeleteCover(String code)
        {
            String thumb = ThumbsDir + code + ".png";
            String cover = CoversDir + code + ".png";

            int c = CoverLoaded(code);
            if (c >= 0)
            {
                LoadedCovers[c].Image.Dispose();
                LoadedCovers.RemoveAt(c);
            }

            c = FullSizeCoverLoaded(code);
            if (c >= 0)
            {
                FullSizeImages[c].Image.Dispose();
                FullSizeImages.RemoveAt(c);
            }

            c = AvaliableCovers.IndexOf(thumb);
            if (c >= 0)
            {
                AvaliableCovers.RemoveAt(c);
            }

            if (File.Exists(thumb))
            {
                IOManager.Delete(thumb);
            }

            if (File.Exists(cover))
            {
                IOManager.Delete(cover);
            }
        }

        //----------------------- Apaga permanentemente várias capas
        public static void DeleteCovers(String[] codes)
        {
            foreach (String code in codes) DeleteCover(code);
        }

        //----------------------- Verifica se existe uma capa para o código
        static bool HasCover(String code)
        {
            foreach (String cover in AvaliableCovers)
            {
                if (String.Compare(Path.GetFileNameWithoutExtension(cover), code, true) == 0) return true;
            }

            return false;
        }

        //----------------------- Carrega as capas de tamanho grande (pode ser bem lento)
        public static void LoadFullSizeCovers()
        {
            if (FullSizeImages.Count > 0) DisposeFullSizeCovers();
            
            String[] covers = Directory.GetFiles(CoversDir, "*.png");
            for (int i = 0; i < covers.Length; i++)
            {
                try
                {
                    LoadedCover lc = new LoadedCover(Path.GetFileNameWithoutExtension(covers[i]), Image.FromFile(covers[i]));
                    FullSizeImages.Add(lc);
                }
                catch (OutOfMemoryException)
                {
                    Log.LogLine(Language.message_corrupted_cover, LogMessageType.Error,
                        Path.GetFileNameWithoutExtension(covers[i]));
                    DeleteCover(Path.GetFileNameWithoutExtension(covers[i]));
                }
            }
        }

        //----------------------- Descarrega as capas de tamanho grande
        public static void DisposeFullSizeCovers()
        {
            while(FullSizeImages.Count > 0)
            {
                FullSizeImages[0].Image.Dispose();
                FullSizeImages.RemoveAt(0);
            }
        }

        public static void DisposeCovers()
        {
            while (LoadedCovers.Count > 0)
            {
                LoadedCovers[0].Image.Dispose();
                LoadedCovers.RemoveAt(0);
            }
        }

        //----------------------- Retorna uma imagem tamanho grande respectiva ao código
        public static Image GetFullSizeCover(String code)
        {
            foreach (LoadedCover cover in FullSizeImages) 
                if (String.Compare(cover.Code, code, true) == 0) return cover.Image;

            return Skin.cover_nocover_big;
        }

        //----------------------- Sincroniza um jogo apenas
        public static void SynchronizeCover(String code, bool force)
        {
            if (!AutoDownloadCovers && !force)
                return;

            code = code.ToUpper();
            if (!CoversToDownload.Contains(code) && !FailedDownloads.Contains(code) && !HasCover(code))
            {
                CoversToDownload.Add(code);
                OperationManager.DownloadCovers();
            }
        }

        //----------------------- Sincroniza Capas dos dispositivos, bibliotecas e enfileira para download
        public static int SynchronizeCovers()
        {
            int i = SynchronizeFromLibraries(false);
            i += SynchronizeFromDevices(false);

            if ((i > 0) && AutoDownloadCovers) OperationManager.DownloadCovers();

            return i;
        }

        //----------------------- Sincroniza Capas apenas das bibliotecas
        public static int SynchronizeFromLibraries(bool download)
        {
            if (!download || !AutoDownloadCovers)
                return 0;

            String code;
            int i = 0;
            foreach (LibraryFile file in Library.Files)
            {
                code = file.Code.ToUpper();
                if (!CoversToDownload.Contains(code) && !FailedDownloads.Contains(code) && !HasCover(code))
                {
                    CoversToDownload.Add(code);
                    i++;
                }
            }

            OperationManager.DownloadCovers();

            return i;
        }

        //----------------------- Sincroniza Capas apenas dos dispositivos
        internal static int SynchronizeFromDevices(bool download)
        {
            if (!download || !AutoDownloadCovers)
                return 0;

            IDrive drive;
            int i = 0;
            int j = 0;
            while ((drive = DeviceManager.GetWBFSDrive(j++)) != null)
            {
                i += SynchronizeFromWBFSDrive((WBFSDrive)drive, false);
            }

            OperationManager.DownloadCovers();

            return i;
        }

        //----------------------- Sincroniza Capas apenas de um drive WBFS
        public static int SynchronizeFromWBFSDrive(WBFSDrive drive, bool download)
        {
            if (!download || !AutoDownloadCovers)
                return 0;

            String code;
            IDisc disc;
            int i = 0;
            int j = 0;

            while ((disc = drive._Device.GetDiscByIndex(j++)) != null)
            {
                code = disc.Code.ToUpper();
                if (!CoversToDownload.Contains(code) && !FailedDownloads.Contains(code) && !HasCover(code))
                {
                    CoversToDownload.Add(code);
                    i++;
                }
            }

            OperationManager.DownloadCovers();

            return i;
        }

        //----------------------- A lista de capas foi atualizada, em FormWBFSSync atualiza a pagina atual do explorer
        public static void OnCoverListChanged()
        {
            CoverListChanged(null, null);
        }

        //----------------------- Operação síncrona de download de capa, será chamada CoversToDownload.Count vezes,
        // Ela não remove a capa da lista, isso fica a cargo do OperationManager
        public static void DownloadCover(String code, ProgressIndicator progress)
        {
            HttpWebRequest request = null;
            HttpWebResponse response = null;
            Stream stream = null;
            IIOContext cover = null;
            byte[] data = null;

            String coverfile = "";
            String thumbfile = "";

            String region = "";
            Boolean isPal = false;
            Boolean lastChance = false;

            switch (code[3])
            {
                case 'E': region = "US"; break; //Americano
                case 'J': region = "JA"; break; //Japonês
                case 'K': region = "KO"; break; //Coreano
                case 'W': region = "ZH"; break; //Chinês
                default: region = UserRegion; isPal = true; break; //PAL
            }

            BUILDSTRING:

            String downloadString = String.Format("{0}wiitdb/artwork/{1}/{2}/{3}.png",
                DownloadHost, Download3DCovers ? "cover3D" : "cover", region, code);            

            try
            {
                request = (HttpWebRequest)WebRequest.Create(downloadString);
                request.Referer = DownloadHost;

                if (response != null) response.Close(); //Garante que outras conexões sejam fechadas
                response = (HttpWebResponse)request.GetResponse();
                stream = response.GetResponseStream();

                data = new byte[8]; //PNG File ID: 89 50 4E 47 0D 0A 1A 0A
                stream.Read(data, 0, 8);

                //---- Verificação de formato

                if ((data[0] != 0x89) || (data[1] != 0x50) || (data[2] != 0x4E) || (data[3] != 0x47) ||
                    (data[4] != 0x0D) || (data[5] != 0x0A) || (data[6] != 0x1A) || (data[7] != 0x0A))
                {
                    Log.LogLine(Language.log_cover_not_found, LogMessageType.Warning, code);

                    //Erro, tenta baixar formato padrão
                    if (isPal && !lastChance)
                    {
                        lastChance = true;
                        region = "EN";
                        goto BUILDSTRING;
                    }

                    //Error here
                    FailedDownloads.Add(code);
                    goto FREE;
                }

                //---- Termina de baixar

                coverfile = CoversDir + code + ".png";
                thumbfile = ThumbsDir + code + ".png";

                cover = IOManager.CreateIOContext(code, coverfile, FileAccess.Write, FileShare.Read, 0,
                    FileMode.Create, EFileAttributes.None);

                if((cover.Result != 0) || (cover.Lock() != IORet.RET_IO_OK))
                {
                    //Erro de IO
                    FailedDownloads.Add(code);
                    goto FREE;
                }

                cover.Write(data, 0, 8);

                data = null;
                data = new byte[1024];
                
                int read = 0;
                while (((read = stream.Read(data, 0, 1024)) != 0) && !progress.Cancel) cover.Write(data, read);

                cover.Close();
                cover = null;

                if (progress.Cancel)
                {
                    IOManager.Delete(coverfile);
                }
                else
                {
                    ResizeImage.ResizeFile(coverfile, thumbfile, CoverWidth, CoverHeight);
                    AvaliableCovers.Add(thumbfile);
                }
            }
            catch (Exception e)
            {
                Log.LogException(e);

                //Erro, tenta baixar formato padrão
                if (isPal && !lastChance)
                {
                    lastChance = true;
                    region = "EN";
                    goto BUILDSTRING;
                }

                FailedDownloads.Add(code);
                if (File.Exists(coverfile)) IOManager.Delete(coverfile);
                if (File.Exists(thumbfile)) IOManager.Delete(thumbfile);
            }

            FREE:

            if(cover != null) cover.Close();
            if (stream != null) stream.Close();
            if (response != null) response.Close();
            data = null;
        }
    }
}
